# encoding: utf-8
# Netease Music API with Ruby
# @author kumho
# @see https://github.com/darknessomi/musicbox/blob/master/NEMbox/api.py
require 'rest-client'
require 'digest'
require 'base64'
require 'openssl'

class Frimusic
  MODULUS = %w(00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7
               b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280
               104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932
               575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b
               3ece0462db0a22b8e7).join
  NONCE = '0CoJUm6Qyw8W8jud'
  PUB_KEY = '010001'

  TIMEOUT   = 10

  # Search type
  SONG      = 1
  ALBUM     = 10
  ARTIST    = 100
  PLAYLIST  = 1000
  USER      = 1002

  def initialize
    @headers = {
      'Accept'          => '*/*',
      'Accept-Encoding' => 'gzip,deflate,sdch',
      'Accept-Language' => 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
      'Connection'      => 'keep-alive',
      'Content-Type'    => 'application/x-www-form-urlencoded',
      'Host'            => 'music.163.com',
      'Referer'         => 'http://music.163.com/',
      'User-Agent'      => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36'
    }
    @cookies = {'appver'=> '1.5.2'}
  end

  # Search
  # Params s: keyword, type: Search Type
  def search(s, type, offset = 0, limit = 100)
    action = 'http://music.163.com/api/search/get/'
    params = {s: s, type: type, offset: offset, limit: limit}
    http_request :post, action, params
  end

  # Get song detail
  # Example: song_id 479524566
  def song_detail(song_id)
    action = "http://music.163.com/api/song/detail/?id=#{song_id}&ids=[#{song_id}]"
    result = http_request :get, action
    result['songs']
  end

  # Get songs detail
  def songs_detail(song_ids = [])
    return [] if song_ids.empty?
    action = "http://music.163.com/api/song/detail?ids=[#{song_ids.join(',')}]"
    result = http_request :post, action
    result['songs']
  end

  # Get lyric of song
  def lyric(song_id)
    action = 'http://music.163.com/api/song/lyric'
    params = {
      id: song_id,
      os: 'osx',
      lv: -1,
      kv: -1,
      tv: -1
    }
    result = http_request :get, action, params
    result['lrc']['lyric'] rescue nil
  end

  # Get a artist
  # Example: artist_id 10557
  def artist(artist_id)
    action = "http://music.163.com/api/artist/#{artist_id}"
    result = http_request :get, action
    result['hotSongs']
  end

  # Get albums of a artist
  # Example: artist_id 10557
  def artist_albums(artist_id, offset = 0, limit = 100)
    action = "http://music.163.com/api/artist/albums/#{artist_id}"
    params = {offset: offset, limit: limit}
    http_request :get, action, params
  end

  # Get a album
  # Example: album_id 32311
  def album(album_id)
    action = "http://music.163.com/api/album/#{album_id}"
    result = http_request :get, action
    result['album']['songs'] rescue nil
  end

  # get playlist detail
  # Example playlist_id 820195717
  def playlist_detail(playlist_id)
    action = "http://music.163.com/api/playlist/detail?id=#{playlist_id}"
    result = http_request :get, action
    result['result']['tracks'] rescue nil
  end

  # Download a mp3 file
  def download(song_id, song_dfsid)
    action = "http://m1.music.126.net/#{encrypted_id(song_id)}/#{song_dfsid}.mp3"
    http_request :get, action
  end

  # Log in
  def login(username, password)
    action = 'https://music.163.com/weapi/login?csrf_token='
    params = {
      username: username,
      password: password,
      rememberLogin: true
    }
    params = encrypted_request(params)
    http_request(:post, action, params)
  end

  def login_by_phone(phone, password)
    action = 'https://music.163.com/weapi/login/cellphone'
    params = {
      phone: phone,
      password: password,
      rememberLogin: true
    }
    params = encrypted_request(params)
    http_request(:post, action, params)
  end

  protected
    def http_request(method, action, params = nil)
      headers = @headers.merge(@cookies).merge(params: params)
      result = RestClient::Request.execute method: method, url: action, timeout: TIMEOUT,
                                  headers: headers
      JSON.parse(result)
    rescue => e
      puts "http_request raise.. #{e.message}"
      []
    end

    # Encrypt song
    def encrypted_id(id)
      magic = '3go8&$8*3*3h0k(2)2'.bytes
      song_id = id.to_s.bytes
      magic_len = magic.size
      song_id.each_with_index do |sid, i|
        song_id[i] = sid ^ magic[i % magic_len]
      end
      song_id = song_id.map(&:chr).join
      result = Digest::MD5.base64digest(song_id)
      result = result.gsub(/\//, '_').gsub(/\+/, '-')
      result.encode('utf-8')
    end

    # Encrypt login request
    # @see https://github.com/stkevintan/nw_musicbox
    # @see https://github.com/darknessomi/musicbox/blob/master/NEMbox/api.py
    def encrypted_request(text)
      text = formated_json(text)
      sec_key = create_secret_key(16)
      enc_text = aes_encrypt(aes_encrypt(text, NONCE), sec_key)
      enc_sec_key = rsa_encrypt(sec_key, PUB_KEY, MODULUS)
      {'params'=> enc_text, 'encSecKey'=> enc_sec_key}
    end

    def aes_encrypt(text, secret_key)
      pad = 16 - text.size % 16
      text = text + pad.chr * pad
      cipher = OpenSSL::Cipher::Cipher.new('AES-128-CBC')
      cipher.encrypt
      cipher.key = secret_key
      cipher.iv = '0102030405060708'
      cipher_text = cipher.update(text)
      Base64.encode64(cipher_text).gsub(/\n/, '').encode('utf-8')
    rescue => e
      puts "aes_encryt raise.. #{e.message}"
      ''
    end

    def rsa_encrypt(text, pub_key, modulus)
      text = text.reverse
      text, pub_key, modulus = text.unpack("H*")[0].to_i(16), pub_key.to_i(16), modulus.to_i(16)
      result = text ** pub_key % modulus
      result = sprintf "%x" % result
      result.rjust(256, '0')
    end

    def create_secret_key(size)
      SecureRandom.hex(size)[0...size]
    end

    def formated_json(params)
      result = '{'
      params.keys.each_with_index do |key, index|
        if params[key].is_a?(Hash)
          result << %Q("#{key.to_s}": #{formated_json(params[key]).to_s})
        else
          result << %Q("#{key.to_s}": "#{params[key].to_s}")
        end
        result << ', ' if index != params.size - 1
      end
      result << '}'
    end
end